#region (c) 2014 Nguyễn, M. Hải - All rights reserved
/*
BLADE VIEW ENGINE

The MIT License (MIT)
Copyright (c) 2014 Nguyễn, M. Hải

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the "Software"), to deal in the Software 
without restriction, including without limitation the rights to use, copy, modify, merge
, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
 substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
DEALINGS IN THE SOFTWARE.
*/
#endregion

#region Record of Change
//             							CHANGE HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | DEVELOPER  | DESCRIPTION                                              |
// -------------------------------------------------------------------------------------
// | 29-Apr-14 | HaiNM      | First creation.                                          |
// -------------------------------------------------------------------------------------
//
#endregion

#region Record of Audit
//             							REVIEW HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | REVIEWER   | NOTES                                                    |
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
//
#endregion

#region Using directives
using System;
using System.Text;

using Thn;
using Thn.IO;
using Thn.Text;
using Thn.Collections;
#endregion

namespace Thn.Web.Blade
{
    /// <summary>
    /// Base parser for parsing BLADE template.
    /// </summary>
#if DEBUG || TESTING
    public
#else
    internal
#endif
 abstract class BladeParserBase
    {
        #region Fields
        CharProcessor mProcessor;
        Stack<Memento> mScopes;
        /// <summary>
        /// Current scope;
        /// </summary>
        Memento mScope;
        State mState;
        StringBuilder mBuilder;
        StringDictionary<bool> mKeywords;

#if DEBUG || TESTING
        bool Trace = true;
        bool TracePushPop = true;
#endif
        #endregion

        #region Properties

        #endregion

        #region On Found Text
        /// <summary>
        /// This method is raised when a literal text is found.
        /// </summary>
        protected abstract void OnFoundText(string text);
        #endregion

        #region On Found Inline Code
        /// <summary>
        /// This method is raised when a inline of code is found.
        /// </summary>
        protected abstract void OnFoundInlineCode(string text);
        #endregion

        #region On Found Code Block
        /// <summary>
        /// This method is raised when a block of codes is found.
        /// </summary>
        protected abstract void OnFoundCodeBlock(string text);
        #endregion

        #region Parse
        /// <summary>
        /// Parse template from the provided reader.
        /// </summary>
        /// <param name="source">Text source.</param>
        public void Parse(string source)
        {
            var reader = new CharReader(source);
            Parse(reader);
        }
        /// <summary>
        /// Parse template from the provided reader.
        /// </summary>
        /// <param name="reader">The reader responsible for reading characters from.</param>
        public void Parse(ICharReader reader)
        {
            mProcessor = new CharProcessor();
            mProcessor.Reader = reader;
            mState = State.Initial;
            mBuilder = new StringBuilder();

            //initial scope
            mScopes = new Stack<Memento>();
            mScope = new Memento();
            mScope.Mode = ParseMode.Text;
            mScopes.Push(mScope);

            while (mState != State.Completed && !mProcessor.IsEnded)
            {
                switch (mScope.Mode)
                {
                    case ParseMode.Text:
                        ParseText();
                        break;
                    case ParseMode.Code:
                        ParseCode();
                        break;
                    case ParseMode.OptionalCode:
                        ParseOptionalCode();
                        break;
                    case ParseMode.Inline:
                        ParseInline();
                        break;
                    case ParseMode.HtmlTagStart:
                        ParseHtmlTagStart();
                        break;
                    case ParseMode.HtmlTag:
                        ParseHtmlTag();
                        break;
                    case ParseMode.HtmlTagEnd:
                        ParseHtmlTagEnd();
                        break;
                    case ParseMode.LiteralText:
                        ParseLiteralText();
                        break;
                    default:
                        throw new NotSupportedException();
                        break;
                }
            }

            //process all trailing scopes if available
            if (mScopes.Count > 0)
            {
                while (mScopes.Count > 0)
                {
                    Pop();
                }
            }
        }
        #endregion

        #region Parse Text
        void ParseText()
        {
            if (mScope.IsWaitingForEndOfLine)
            {
                #region Waiting for end of line

                var marker = mProcessor.ReadUntil(mBuilder, true, false, true, '@', '\r', 'n');
                if (marker == '@')
                {
                    #region Inline

                    var m = new Memento();
                    m.Mode = ParseMode.Inline;
                    Push(m);

                    #endregion
                }
                else if (marker == '\r' || marker == '\n')
                {
                    #region End of line

                    //completed text line inside code block -> pop scope
                    Pop();

                    #endregion
                }

                #endregion
            }
            else
            {
                #region Normal case

                //read everything as text, until a @ is found
                mProcessor.ReadUntil(mBuilder, true, false, false, '@');

                //determine what to do based on the next character
                var next = mProcessor.Next;
                if (next > -1)
                {
                    #region Has more

                    if (next == '{')
                    {
                        #region Found a code block start

                        //push new scope
                        var m = new Memento();
                        m.Mode = ParseMode.Code;
                        m.DiscardBlockEnd = true;
                        m.BracketCount = 1;
                        Push(m);

                        //consume current character
                        mProcessor.Skip(2);

                        #endregion
                    }
                    else if (next == '*')
                    {
                        #region Found a comment section

                        //consume current character
                        mProcessor.Skip(2);

                        //skip until a closing comment *@ is found
                        mProcessor.ReadUntil(null, true, false, true, "*@");

                        #endregion
                    }
                    else if (next == '@')
                    {
                        #region Escaped @ -> keep one

                        mBuilder.Append('@');
                        mProcessor.Skip(2);

                        #endregion
                    }
                    else if (ParseText_GuessInline())
                    {
                        #region Found inline code

                        mProcessor.Skip();

                        //push new scope
                        var m = new Memento();
                        m.Mode = ParseMode.Inline;
                        Push(m);

                        #endregion
                    }
                    else
                    {
                        #region Everything else ought to be text

                        mBuilder.Append('@');
                        mProcessor.Skip();

                        #endregion
                    }
                    #endregion
                }
                else
                {
                    #region Ended

                    Ended();

                    #endregion
                }

                #endregion
            }
        }
        #endregion

        #region Parse Text - Guess Inline
        /// <summary>
        /// Attempt to guess whether we found an inline code or not.
        /// <para>Note: this is applicable only in Text mode.</para>
        /// </summary>
        bool ParseText_GuessInline()
        {
            bool result = false;
            char prev = (char)mProcessor.Previous;
            char current = (char)mProcessor.Current;
            char next = (char)mProcessor.Next;

            /*
             * Valid inline examples:
             *      @myParameter
             *      @DateTime.Now
             *      @DateTime.Now.ToString()
             *      @DateTime.Now.ToString("dd/mm/yy")
             *      <div>@me</div>
             *      
             * Invalid inline examples:
             *      user@test.com
             *      @@myParameter
             *      @ me
             *      
             * Rules for guessing an inline:
             *      + current is '@'
             *      + next is not '@' (escaped form)
             *      + next is letter (valid names start with letter or _ )
             *      + previous is blank or closing tag
             */

            if (current == '@' && next != '@')
            {
                if (char.IsLetter(next) || next == '_')
                {
                    if (prev == '>' || IsEmptyChar(prev))
                    {
                        result = true;
                    }
                }
            }

            return result;
        }
        #endregion

        #region Parse Inline
        void ParseInline()
        {
            //read while acceptable
            bool isInsideMethodCall = false;
            bool hasMethodSignature = false;
            mProcessor.ReadWhile(mBuilder, (c) =>
                {
                    bool accept = (c == '.')
                                || char.IsLetter(c)
                                || char.IsDigit(c)
                                || (c == '(')
                                || (c == ')')
                                ;
                    if (c == '(') isInsideMethodCall = true;

                    if (isInsideMethodCall)
                    {
                        if (c == ')')
                        {
                            isInsideMethodCall = false;
                            hasMethodSignature = true;
                        }
                    }

                    //handle special case: ", \, space, comma
                    if (!accept)
                    {
                        if (isInsideMethodCall)
                        {
                            accept = true;
                        }
                    }

                    return accept;
                });

            //determine if this is a keyword
            var value = mBuilder.ToString();
            if (mKeywords.ContainsKey(value))
            {
                #region This is a keyword, not an inline

                //handle optional blocks
                bool hasOptionalBlock = false;
                if (value == "if")
                {
                    mScope.ExpectedKeyword = "else";
                    hasOptionalBlock = true;
                }
                else if (value == "do")
                {
                    mScope.ExpectedKeyword = "while";
                    hasOptionalBlock = true;
                }

                if (hasOptionalBlock)
                {
                    #region Need to parse optional block

                    //flush existing text
                    RaiseMode(ParseMode.Code);

                    //change my scope to parsing optional block
                    mScope.Mode = ParseMode.OptionalCode;

                    //push a new scope to parse the main block
                    var m = new Memento();
                    m.Mode = ParseMode.Code;
                    mScopes.Push(m);//manually push so that no event is raised here
                    mScope = m;

                    #endregion
                }
                else
                {
                    #region No need to parse optional block

                    //change my scope to parsing code
                    mScope.Mode = ParseMode.Code;

                    #endregion
                }

                #endregion
            }
            else
            {
                #region Not a keyword -> indeed an inline

                if (hasMethodSignature)
                {
                    #region Special case: guess inline method call

                    if (mProcessor.Current == ';')
                    {
                        //found an inline method call instead of inline property -> switch to code
                        mScope.Mode = ParseMode.Code;

                        //consume comma
                        mProcessor.Advance();

                        //append comma to result
                        mBuilder.AppendLine(";");
                    }

                    #endregion
                }

                Pop();

                #endregion
            }
        }
        #endregion

        #region Parse Code
        void ParseCode()
        {
            int next;
            char c;
            Memento m;

            //read everything as code, until
            var marker = mProcessor.ReadUntil(mBuilder, true, false, false,
                '{', '}', //begin, end of multi-line code block
                '/',        //comment start
                '<',        //tag start
                '@'        //text line or comment start
                );

            switch (marker)
            {
                #region Begin of code block
                case '{':

                    if (mScope.ConsumeNextBlockStart)
                    {
                        //turn off consume flag so that next time will be normal again
                        mScope.ConsumeNextBlockStart = false;
                    }

                    mScope.BracketCount++;

                    //append this character
                    c = (char)marker;
                    mBuilder.Append(c);
                    mProcessor.Advance();

                    break;
                #endregion

                #region End of code block
                case '}':

                    //append to builder if necessary
                    if (!mScope.DiscardBlockEnd || mScope.BracketCount > 1)
                    {
                        c = (char)marker;
                        mBuilder.Append(c);
                    }

                    //handle optional block
                    if (mScope.IsExpectingKeyword)
                    {
                        if (mProcessor.Current == ';' && mScope.BracketCount == 1)
                        {
                            mBuilder.Append(';'); //special case: while ( );
                            mProcessor.Skip();
                        }
                    }

                    mScope.BracketCount--;

                    mProcessor.Advance();
                    if (mScope.BracketCount == 0) Pop();

                    break;
                #endregion

                #region Comment start
                case '/':
                    next = mProcessor.Next;
                    mProcessor.Advance();

                    if (next == '/')
                    {
                        #region Single line comment

                        mProcessor.ReadUntilEndOfLine(null);

                        #endregion
                    }
                    else if (next == '*')
                    {
                        #region Multi-line comment

                        mProcessor.ReadUntil(null, true, false, true, "*/");

                        #endregion
                    }
                    else
                    {
                        #region Not a comment

                        //put the character back
                        mBuilder.Append('/');

                        #endregion
                    }

                    break;
                #endregion

                #region Tag start
                case '<':

                    next = mProcessor.Next;
                    var prev = (char)mProcessor.Previous;

                    bool isTag = true;

                    if (next == ' ') isTag = false;
                    else if (char.IsLetterOrDigit(prev)) isTag = false;

                    if (isTag)
                    {
                        #region Found a possible tag start

                        //push mode
                        m = new Memento();
                        m.Mode = ParseMode.HtmlTagStart;
                        Push(m);

                        #endregion
                    }

                    //append to builder
                    mBuilder.Append('<');

                    mProcessor.Advance();

                    break;
                #endregion

                #region Text line or comment start
                case '@':
                    next = mProcessor.Next;

                    if (next == '@')
                    {
                        #region Escaped @ -> keep one

                        mBuilder.Append('@');
                        mProcessor.Skip(2);

                        #endregion
                    }
                    else if (ParseText_GuessInline())
                    {
                        #region Found inline code

                        mProcessor.Advance();

                        //push new scope                        
                        Push(new Memento()
                            {
                                Mode = ParseMode.Inline
                            });

                        #endregion
                    }
                    else if (next == ':')
                    {
                        #region Text line -> switch mode

                        //skip next
                        mProcessor.Skip(2);

                        //push mode
                        m = new Memento();
                        m.Mode = ParseMode.Text;
                        m.IsWaitingForEndOfLine = true;
                        Push(m);

                        #endregion
                    }
                    else if (next == '*')
                    {
                        #region Comment

                        //skip next
                        mProcessor.Skip(2);

                        //skip until *@
                        mProcessor.ReadUntil(null, true, false, true, "*@");

                        #endregion
                    }
                    else if (next == '!')
                    {
                        #region Literal text block

                        mProcessor.Advance(2);
                        Push(new Memento()
                            {
                                Mode = ParseMode.LiteralText
                            });

                        #endregion
                    }
                    else
                    {
                        #region Normal case

                        mProcessor.Advance();

                        //append back to result
                        mBuilder.Append('@');

                        #endregion
                    }

                    break;
                #endregion

                #region EOF
                case -1:
                    break;
                #endregion

                #region Unsupported
                default:
                    RaiseError("Invalid format");
                    break;
                #endregion
            }
        }
        #endregion

        #region Parse Optional Code
        void ParseOptionalCode()
        {
            //read the next word
            bool keywordStarted = false;
            string keyword = "";
            mProcessor.ReadWhile(mBuilder, (token) =>
            {
                bool accepted = true;

                if (keywordStarted)
                {
                    #region Already started reading keyword -> accept alpha

                    if (char.IsLetter(token))
                    {
                        accepted = char.IsLetter(token);
                        keyword += token;
                    }
                    else accepted = false;

                    #endregion
                }
                else
                {
                    #region Not started yet

                    if (char.IsLetter(token))
                    {
                        keyword += token;
                        keywordStarted = true;
                    }
                    else accepted = char.IsControl(token) || token == ' ';

                    #endregion
                }

                return accepted;
            });

            //handle keyword
            if (keyword == mScope.ExpectedKeyword)
            {
                #region Found expected keyword -> switch to code

                mScope.Mode = ParseMode.Code;
                mScope.IsExpectingKeyword = true; //turn on flag for special case in ParseCode
                mScope.ExpectedKeyword = null; //we no longer want to compare a keyword

                #endregion
            }
            else
            {
                #region Expected keyword not found -> revert back to normal case

                mScope.Mode = ParseMode.Text;
                mScope.IsExpectingKeyword = false;
                mScope.ExpectedKeyword = null;

                #endregion
            }
        }
        #endregion

        #region Parse Html Tag Start
        void ParseHtmlTagStart()
        {
            if (mScope.IsEscaped)
            {
                var marker = mProcessor.ReadUntil(mBuilder, true, false, true, '"', '@');
                if (marker == '"')
                {
                    #region Ending quote

                    mBuilder.Append('"');
                    mScope.IsEscaped = false;

                    #endregion
                }
                else
                {
                    #region Code marker

                    //push new scope
                    var m = new Memento();
                    m.Mode = ParseMode.Inline;
                    Push(m);

                    #endregion
                }
            }
            else
            {
                mProcessor.ReadWhile(mBuilder, (c) =>
                    {
                        bool accepted;

                        accepted = c == ' '
                                || char.IsLetter(c)
                                || char.IsDigit(c)
                                || char.IsControl(c)
                                ;

                        return accepted;
                    });

                var marker = mProcessor.Current;
                if (marker == '>')
                {
                    #region End of tag marker

                    //switch to next state
                    mScope.Mode = ParseMode.HtmlTag;

                    //append
                    mBuilder.Append('>');
                    mProcessor.Advance();

                    #endregion
                }
                else if (marker == '/' && mProcessor.Next == '>')
                {
                    #region End of self-contained tag

                    mBuilder.Append("/>");
                    mProcessor.Skip(2);
                    Pop();

                    #endregion
                }
                //else if (marker == '"' || marker == '\'')
                else if (marker == '"')
                {
                    #region Quotes

                    mScope.IsEscaped = true;
                    mBuilder.Append((char)marker);
                    mProcessor.Advance();

                    #endregion
                }
                else if (marker == ' ' || marker == '=')
                {
                    #region Acceptable characters within HTML tag start

                    mBuilder.Append((char)marker);
                    mProcessor.Advance();

                    #endregion
                }
                else
                {
                    #region Unacceptable characters -> not an HTML tag -> revert & pop

                    //var c = (char)marker;

                    mScopes.Pop();
                    mScope = mScopes.Last;

                    #endregion
                }
            }
        }
        #endregion

        #region Parse Html Tag
        void ParseHtmlTag()
        {
            var marker = mProcessor.ReadUntil(mBuilder, true, false, true, '<', '@');

            if (marker == '<')
            {
                #region Start of tag marker

                var current = mProcessor.Current;
                if (current == '/')
                {
                    #region End tag marker

                    mScope.Mode = ParseMode.HtmlTagEnd;
                    mBuilder.Append('<');

                    #endregion
                }
                else
                {
                    #region New start tag

                    //push scope
                    var m = new Memento();
                    m.Mode = ParseMode.HtmlTagStart;
                    Push(m);

                    mBuilder.Append('<');

                    #endregion
                }

                #endregion
            }
            else if (marker == '@')
            {
                #region Inline marker

                var m = new Memento();
                m.Mode = ParseMode.Inline;
                Push(m);

                #endregion
            }
        }
        #endregion

        #region Parse Html Tag End
        void ParseHtmlTagEnd()
        {
            mProcessor.ReadUntil(mBuilder, true, true, true, '>');

            //end tag completed -> pop
            Pop();
        }
        #endregion

        #region Parse Literal Text
        void ParseLiteralText()
        {
            mProcessor.ReadUntil(mBuilder, true, false, false, '!');
            if (!mProcessor.IsEnded)
            {
                var next = mProcessor.Next;
                mProcessor.Advance();

                if (next == '@')
                {
                    if (mProcessor.Next != '@')
                    {
                        #region Not escaped -> found literal text end

                        mProcessor.Advance();
                        Pop();

                        #endregion
                    }
                    else
                    {
                        #region Escaped -> keep processing

                        //append one sign
                        mBuilder.Append('@');
                        mProcessor.Advance();

                        #endregion
                    }
                }
                else
                {
                    #region Not ended -> keep processing

                    mBuilder.Append('!');

                    #endregion
                }
            }
        }
        #endregion

        #region Found Text
        void FoundText()
        {
            if (mBuilder.Length > 0)
            {
                var s = mBuilder.ToString();

                if (!IsEmptyString(s))
                {
                    #region Tracing
#if DEBUG || TESTING
                    if (Trace)
                    {
                        Log.Trace(this, Logging.LogType.Debug, null, "Row: {0} Column: {1} Position: {2} - Text: {3}", mProcessor.Row, mProcessor.Column, mProcessor.Position, s);
                    }
#endif
                    #endregion

                    OnFoundText(s);
                }

                mBuilder = new StringBuilder();
            }
        }
        #endregion

        #region Found Code Inline
        void FoundInlineCode()
        {
            if (mBuilder.Length > 0)
            {
                var s = mBuilder.ToString();

                #region Tracing
#if DEBUG || TESTING
                if (Trace)
                {
                    Log.Trace(this, Logging.LogType.Debug, null, "Row: {0} Column: {1} Position: {2} - Inline: {3}", mProcessor.Row, mProcessor.Column, mProcessor.Position, s);
                }
#endif
                #endregion

                OnFoundInlineCode(s);
                mBuilder = new StringBuilder();
            }
        }
        #endregion

        #region Found Code Block
        void FoundCodeBlock()
        {
            if (mBuilder.Length > 0)
            {
                var s = mBuilder.ToString();

                if (!IsEmptyString(s))
                {
                    #region Tracing
#if DEBUG || TESTING
                    if (Trace)
                    {
                        Log.Trace(this, Logging.LogType.Debug, null, "Row: {0} Column: {1} Position: {2} - Code: {3}", mProcessor.Row, mProcessor.Column, mProcessor.Position, s);
                    }
#endif
                    #endregion

                    OnFoundCodeBlock(s);
                }

                mBuilder = new StringBuilder();
            }
        }
        #endregion

        #region Ended
        /// <summary>
        /// Process end of file.
        /// </summary>
        void Ended()
        {
            mState = State.Completed;

            ////append trailing text if available
            //if (mBuilder.Length > 0)
            //{
            //    switch (mScope.Mode)
            //    {
            //        case ParseMode.Text:
            //            OnFoundText(mBuilder.ToString());
            //            break;
            //        case ParseMode.Code:
            //            OnFoundCodeBlock(mBuilder.ToString());
            //            break;
            //        default:
            //            throw new NotSupportedException();
            //            break;
            //    }
            //}
        }
        #endregion

        #region Push
        void Push(Memento scope)
        {
            #region Tracing
#if DEBUG || TESTING
            if (TracePushPop)
            {
                Log.Trace(this, Logging.LogType.Debug, null, "PUSH row: {0} column: {1} scope: {2} -> {3} - builder: {4}", mProcessor.Row, mProcessor.Column, mScopes.Count > 0 ? mScopes.Last.Mode.ToString() : "null", scope.Mode, mBuilder.ToString());
            }
#endif
            #endregion

            var lastMode = mScopes.Last.Mode;

            if (lastMode != scope.Mode)
            {
                #region New scope is different from last

                RaiseMode(lastMode);

                #endregion
            }
            #region Defensive tracing
            else
            {
                //same mode -> keep going
            }
            #endregion

            mScopes.Push(scope);
            mScope = scope;
        }
        #endregion

        #region Pop
        void Pop()
        {
            var prevMode = mScope.Mode;
            mScopes.Pop();
            mScope = mScopes.Last;

            RaiseMode(prevMode);

            #region Tracing
#if DEBUG || TESTING
            if (TracePushPop)
            {
                Log.Trace(this, Logging.LogType.Debug, null, "POP row: {0} column: {1} scope: {2} -> {3} - builder: {4}", mProcessor.Row, mProcessor.Column, prevMode, mScopes.Count > 0 ? mScopes.Last.Mode.ToString() : "null", mBuilder.ToString());
            }
#endif
            #endregion
        }
        #endregion

        #region Raise Mode
        void RaiseMode(ParseMode mode)
        {
            switch (mode)
            {
                case ParseMode.Text:
                case ParseMode.LiteralText:
                case ParseMode.HtmlTagEnd:
                    FoundText();
                    break;
                case ParseMode.Inline:
                    FoundInlineCode();
                    break;
                case ParseMode.Code:
                case ParseMode.OptionalCode:
                    FoundCodeBlock();
                    break;
                case ParseMode.HtmlTagStart:
                case ParseMode.HtmlTag:

                    //note: this scenario means the input source has invalid format.
                    //we'll treat it as text
                    FoundText();
                    break;
                default:
                    throw new NotSupportedException();
                    break;
            }
        }
        #endregion

        #region Raise Error
        [System.Diagnostics.DebuggerHidden]
        void RaiseError(string message, params object[] args)
        {
            if (args != null && args.Length > 0) message = string.Format(message, args);

            //append info
            message += string.Format(" (Position: {0} Row: {1} Column: {2}", mProcessor.Position, mProcessor.Row, mProcessor.Column);

            //throw
            throw new Exception(message);
        }
        #endregion

        #region Is Empty String
        bool IsEmptyString(string s)
        {
            bool result = true;

            if (s != null)
            {
                var chars = s.ToCharArray();
                var length = chars.Length;
                Char c;
                for (int i = 0; i < length; i++)
                {
                    c = chars[i];
                    if (c != ' '
                        && !char.IsControl(c)
                        )
                    {
                        result = false;
                        break;
                    }
                }
            }

            return result;
        }
        #endregion

        #region Is Empty Char
        bool IsEmptyChar(char c)
        {
            return c == ' '
                || char.IsControl(c)
                ;
        }
        #endregion

        #region Build Keywords
        void BuildKeywords()
        {
            mKeywords = new StringDictionary<bool>();
            var d = mKeywords;

            d["if"] = true;
            d["switch"] = true;
            d["for"] = true;
            d["while"] = true;
            d["do"] = true;
            d["foreach"] = true;
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Create a new instance.
        /// </summary>
        public BladeParserBase()
            : base()
        {
            BuildKeywords();
        }
        #endregion

        #region Inner class - Mode
        enum ParseMode
        {
            /// <summary>
            /// Processing text.
            /// </summary>
            Text,
            /// <summary>
            /// Processing inline code.
            /// </summary>
            Inline,
            /// <summary>
            /// Processing code.
            /// </summary>
            Code,
            /// <summary>
            /// Processing a code block that might or might not exist.
            /// </summary>
            OptionalCode,
            /// <summary>
            /// Processing HTML tag header.
            /// </summary>
            HtmlTagStart,
            /// <summary>
            /// Processing HTML tag inner text.
            /// </summary>
            HtmlTag,
            /// <summary>
            /// Processing HTML tag footer.
            /// </summary>
            HtmlTagEnd,
            /// <summary>
            /// Processing a block of literal text.
            /// </summary>
            LiteralText
        }
        #endregion

        #region Inner class - Memento
        class Memento
        {
            public ParseMode Mode;
            public bool IsWaitingForEndOfLine;
            public bool DiscardBlockEnd;
            public bool ConsumeNextBlockStart;
            public bool IsExpectingKeyword;
            public string ExpectedKeyword;
            public bool IsEscaped;
            public int BracketCount;
            public ParseMode PreviousMode = ParseMode.Text;

            public override string ToString()
            {
                return string.Format("Mode: {0}", Mode);
            }
        }
        #endregion

        #region Inner class - State
        enum State
        {
            Initial,
            Completed
        }
        #endregion
    }
}
